package nl.weeaboo.sh.level;

import java.awt.Rectangle;
import java.awt.event.KeyEvent;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import nl.weeaboo.game.gl.GLDraw;
import nl.weeaboo.game.gl.GLManager;
import nl.weeaboo.game.input.UserInput;
import nl.weeaboo.sh.Game;
import nl.weeaboo.sh.GameFrame;
import nl.weeaboo.sh.io.AreaLoader;
import nl.weeaboo.sh.model.AreaModel;
import nl.weeaboo.sh.model.RoomModel;
import nl.weeaboo.sh.model.TileSetModel;
import nl.weeaboo.sh.objects.Boss;
import nl.weeaboo.sh.objects.Effect;
import nl.weeaboo.sh.objects.Platform;
import nl.weeaboo.sh.objects.Player;
import nl.weeaboo.sh.objects.Shot;
import nl.weeaboo.sh.objects.Sprite;
import nl.weeaboo.sh.view.Camera;
import nl.weeaboo.sh.view.OSD;
import timon.common.Log;
import timon.common.collections.RPlusTree;

public class Level extends AbstractLevel {
	
	public static final int tileSize = 32;

	protected final int startLevel;
	protected final int startArea;
	
	protected Game game;
	protected Camera camera;
	protected OSD osd;
	protected Player player;
	protected Boss boss;
	protected int frame;
	protected LevelMenu menu;
	
	protected List<Sprite> sprites;
	protected List<Sprite> spritesAddList;
	protected List<Effect> effects;
	protected List<Effect> effectsAddList;

	protected ActiveTiles activeTiles;
	protected AreaModel area;

	private RPlusTree roomRTree;	
	private Map<RoomModel, Room> rooms;
	private Map<String, TileSet> tileSets;
	
	public Level(Game g, int startLevel, int startArea) {
		super(g);
		
		game = g;
		this.startLevel = startLevel;
		this.startArea = startArea;
		
		camera = new Camera(game, this);
		sprites = new LinkedList<Sprite>();
		spritesAddList = new ArrayList<Sprite>();
		effects = new LinkedList<Effect>();
		effectsAddList = new ArrayList<Effect>();
		activeTiles = new ActiveTiles(128, 64);
		
		roomRTree = new RPlusTree();
		rooms = new HashMap<RoomModel, Room>();
		tileSets = new HashMap<String, TileSet>();
	}
	
	//Functions
	public void loadResources() {
		super.loadResources();
				
		frame = 0;
		roomRTree.clear();
		
		AreaLoader al = new AreaLoader();
		try {
			area = al.load(game, game.getInputStream(String.format("level/%d/area%02d.xml",
					startLevel, startArea)));
		} catch (IOException e) {
			Log.showError(e);
			area = new AreaModel("_error_");
		}
		
		for (RoomModel rm : area.getRooms()) {
			roomRTree.add(rm.getBounds(), rm);
		}
		
		for (TileSetModel tsm : area.getTileSets().values()) {
			TileSet ts = new TileSet(tsm, game.getImageStore(), game.getImage(
					String.format("tiles/%s.png", tsm.getId()), true), null);
			tileSets.put(ts.getId(), ts);
		}

		player = new Player(game, this);
		player.setXY(400, 0);
		sprites.add(player);
		
		camera.setXY(400, 240);

		boss = null;
		
		osd = new OSD(game, this);
	}
	public void unloadResources() {
		while (!effectsAddList.isEmpty()) effectsAddList.remove(0).dispose();
		while (!effects.isEmpty()) effects.remove(0).dispose();
		while (!spritesAddList.isEmpty()) spritesAddList.remove(0).dispose();
		while (!sprites.isEmpty()) sprites.remove(0).dispose();

		game.getCharaImageStore().unloadGLImages();
		for (Room r : rooms.values()) {
			if (r != null) {
				r.unloadResources();
			}
		}
		rooms.clear();
		
		osd = null;
		
		super.unloadResources();
	}
	
	public void addSprite(Sprite s) {
		spritesAddList.add(s);
	}
	public void removeSprite(Sprite s) {
		if (s == boss) {
			boss = null;
		}
		
		if (spritesAddList.remove(s) || sprites.remove(s)) {
			if (!s.isDestroyed()) s.destroy();
			s.dispose();
		}
	}
	public void addEffect(Effect e) {
		effectsAddList.add(e);
	}
	public void removeEffect(Effect e) {
		if (effectsAddList.remove(e) || effects.remove(e)) {
			if (!e.isDestroyed()) e.destroy();
			e.dispose();
		}
	}
	
	public void onBossDefeated(Boss boss) {
	}
	public void onPlayerDeath() {
		camera.setXY(player.getX(), player.getY());
	}
	public void onPlayerChangeRoom(Room oldRoom, Room newRoom) {
	}

	public static int toTileCoord(float p) {
		return toTileCoord(Math.round(p));
	}
	public static int toTileCoord(int p) {
		return (p + (p < 0 ? -Level.tileSize+1 : 0)) / Level.tileSize;
	}
	public static int fromTileCoord(int p) {
		return p * Level.tileSize;
	}
	
	public void update(UserInput input, float dt) {
		if (input.consumeKey(KeyEvent.VK_ESCAPE)) {
			setMenu(menu == null ? LevelMenu.PAUSE : null); //Toggle menu
		}		
		if (menu != null) {
			if (osd != null) osd.updateMenu(input, dt);
			return;
		}
		
		frame++;
		
		int cx = toTileCoord(camera.getX());
		int cy = toTileCoord(camera.getY());
		int w = activeTiles.getWidth();
		int h = activeTiles.getHeight();
		Rectangle activeBounds = new Rectangle(cx-w/2, cy-h/2, w, h);
		updateActiveTiles(cx, cy, activeBounds);

		//Update tiles
		Tile tiles[][] = activeTiles.getTiles();
		for (int x = 0; x < tiles.length; x++) {
			for (int y = 0; y < tiles[x].length; y++) {
				if (tiles[x][y] != null) tiles[x][y].update(input, dt);
			}
		}
		
		//Update sprites
		for (Sprite s : spritesAddList) {
			if (s instanceof Platform) {
				sprites.add(0, s);
			} else {
				sprites.add(s);
			}
		}
		spritesAddList.clear();
		
		for (Sprite s : sprites) {
			if (!s.isDestroyed()) {
				int sx = toTileCoord(s.getX());
				int sy = toTileCoord(s.getY());
				Room room = getRoom(sx, sy);
				if (area.getSpriteBounds().contains(sx, sy) && activeBounds.contains(sx, sy)) {
					s.update(input, dt);
				} else {
					//Destroy sprites that are outside of all room bounds AND visible bounds					
					if (room == null && s instanceof Shot) {
						s.destroy();
					}
				}
			}
		}

		//Remove dead sprites
		for (Iterator<Sprite> it = sprites.iterator(); it.hasNext(); ) {
			Sprite s = it.next();
			if (s.isDestroyed()) {
				it.remove();
				s.dispose();
			}
		}
		
		//Update effects
		effects.addAll(effectsAddList);
		effectsAddList.clear();
		
		for (Effect e : effects) {
			if (!e.isDestroyed()) {
				e.update(input, dt);
			}
		}
		
		//Remove dead effects
		for (Iterator<Effect> it = effects.iterator(); it.hasNext(); ) {
			Effect e = it.next();
			if (e.isDestroyed()) {
				it.remove();
				e.dispose();
			}
		}

		//Update universe
		if (getUniverseAdapter() != null) {
			getUniverseAdapter().update(input);
		}
		
		camera.update(activeTiles.getCameraBounds());
		
		if (osd != null) osd.update(input, dt);
	}
	protected void updateActiveTiles(int cx, int cy, Rectangle activeBounds) {		
		activeTiles.updateTiles(this, cx, cy);
		for (Iterator<Entry<RoomModel, Room>> i = rooms.entrySet().iterator(); i.hasNext(); ) {
			Entry<RoomModel, Room> entry = i.next();
			Room r = entry.getValue();
			if (r == null) {
				continue;
			}
			
			Rectangle roomBounds = new Rectangle(r.getX(), r.getY(), r.getWidth(), r.getHeight());
			if (roomBounds.x >= activeBounds.x+activeBounds.width
					|| roomBounds.y >= activeBounds.y+activeBounds.height
					|| roomBounds.x+roomBounds.width < activeBounds.x
					|| roomBounds.y+roomBounds.height < activeBounds.y)
			{
				r.unloadResources();
				entry.setValue(null);
			}
		}		
	}
	
	public void drawBackground(GLManager glm, int w, int h) {
	}
	public void drawForeground(GLManager glm, int w, int h) {
		if (osd != null) osd.draw(glm, w, h);
		
		//Fade in slowly to mask images loading asynchronously
		if (menu == null) {
			glm.setTexture(null);
			glm.pushColor();
			if (frame < 10) {
				glm.setColor(0, 0, 0, 1);
				GLDraw.fillRect(glm.getGL(), 0, 0, w+1, h+1);
			} else if (frame <= 30) {
				glm.setColor(0, 0, 0, 1 - (frame-10) / (30f-10f));
				GLDraw.fillRect(glm.getGL(), 0, 0, w+1, h+1);
			}
			glm.popColor();
		}
	}
	
	//Getters
	public Camera getCamera() { return camera; }
	public List<Sprite> getSprites() { return sprites; }
	public List<Effect> getEffects() { return effects; }
	public Player getPlayer() { return player; }
	public ActiveTiles getActiveTiles() { return activeTiles; }
	public TileSet getTileSet(String id) { return tileSets.get(id); }
	public AreaModel getArea() { return area; }
	public LevelMenu getMenu() { return menu; }
	public Rectangle getVisibleRect() {
		int cx = camera.getX();
		int cy = camera.getY();
		float cs = camera.getScale();
		
		int minX = (int)Math.ceil(cx - (GameFrame.defaultWidth >>> 1) / cs);
		int maxX = (int)Math.floor(cx + (GameFrame.defaultWidth >>> 1) / cs);
		int minY = (int)Math.ceil(cy - (GameFrame.defaultHeight >>> 1) / cs);
		int maxY = (int)Math.floor(cy + (GameFrame.defaultHeight >>> 1) / cs);	

		return new Rectangle(minX, minY, maxX-minX, maxY-minY);
	}
	
	/** @return The room with the specified ID if one is instantiated */ 
	public Room getRoom(String id) {
		for (Room r : rooms.values()) {
			if (r != null && id.equals(r.getId())) {
				return r;
			}
		}
		return null;
	}

	public Room getRoom(int x, int y) {
		RoomModel rm = (RoomModel)roomRTree.getValue(x, y);
		if (rm != null) {
			boolean wasAlreadyloaded = rooms.containsKey(rm);
			Room r = rooms.get(rm);
			if (r == null && activeTiles.getBounds().intersects(rm.getBounds())) {
				rooms.put(rm, r = new Room(this, area, rm));
				r.loadResources(!wasAlreadyloaded);
			}
			return r;
		}
		return null;
	}
	public Boss getBoss() { return boss; }
	
	public boolean isForegroundOpaque() { return osd != null && osd.isOpaque(); }
	
	//Setters	
	public void setBoss(Boss b) {
		if (boss != null) boss.dispose();
		addSprite(boss = b);
	}
	public void setMenu(LevelMenu m) {
		if (menu != m) {
			LevelMenu oldMenu = menu;
			menu = m;
			
			osd.onMenuChanged(oldMenu, m);
		}
	}
	
}
